var utils = require("./util.js")
var ProxyPdu = require("./proxyPdu.js").ProxyPdu
var ECDH = require("./ecdh.js")
var Timer = require("./timer.js")

const ProvisioningErrorCodes = {
  "0": "success",
  "01": "Invalid PDU",
  "02": "Invalid Format",
  "03": "Unexpected PDU",
  "04": "Confirmation Failed",
  "05": "Out of Resources ",
  "06": "Decryption Failed",
  "07": "Unexpected Error",
  "08": "Cannot AssignAddresses",
  "ff": "operate time out"
}
function getResponseMessage() {
  if (arguments.length == 1) return { code: arguments[0], message: ProvisioningErrorCodes[arguments[0]] };
  if (arguments.length == 2) {
    if (typeof (arguments[1]) == "string") {
      return { code: arguments[0], message: arguments[1] };
    }
    if (typeof (arguments[1]) == "object") {
      return { code: arguments[0], data: arguments[1] };
    }
  }
  if (arguments.length == 3) return { code: arguments[0], message: arguments[1], data: arguments[2] };
}
class ProvisioningProcess {
  constructor(blemash,name) {
    this.pdu;
    this.next;
    this.name=name
    this.blemash = blemash;
  }
  setPdu(pdu) {
    this.pdu = pdu;
  }
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, this.name));
    Provisioning.prototype.currentProcess=this;
  }
  processAfter(pdu) {
    if (!pdu.isFailed()) {
      this.setPdu(pdu.parameters);
      this.nextProcess();
    } else {
      let next = this;
      while (next.next != null) {
        next = next.next;
      }
      let errorCode = pdu.parameters.errorCode;
      if (next.error) {
        next.error(errorCode);
      }
    }
  }
  nextProcess() {
    if (this.blemash.provisioning_cancle) return;
    if (this.next != null) {
      this.next.process();
    } else {
      if (this.complete) this.complete();
    }
  }
  then(provisioningProcess) {
    this.next = provisioningProcess;
    return this.next;
  }
  catch(error) {
    this.error = error;
    return this;
  }
  whenComplete(complete) {
    this.complete = complete;
    return this;
  }
  writeProvisionValue() {
    this.blemash.writeProvisionValue(this, this.pdu.toProxyPdu());
  }
}

class Capabilities extends ProvisioningProcess { }
class PublicKeyReceive extends ProvisioningProcess { }
class RandomReceive extends ProvisioningProcess { }
class ConfirmationReceive extends ProvisioningProcess {}
class Complete extends ProvisioningProcess { }

class Invite extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "invite"));
    var pdu = ProxyPdu.prototype.deriveProvisioningInvite({});
    this.setPdu(pdu);
    this.writeProvisionValue();
  }
}

class Start extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "start"));
    var pdu = ProxyPdu.prototype.deriveProvisioningStart({});
    this.setPdu(pdu);
    this.writeProvisionValue();
  }
}

class GenerateKeys extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "generateKeys"));
    [this.privateKey, this.publicKeyX, this.publicKeyY] = ECDH.prototype.generateKeys();
    while (this.publicKeyX.toString(16).length != 64 || this.publicKeyY.toString(16).length != 64) {
      [this.privateKey, this.publicKeyX, this.publicKeyY] = ECDH.prototype.generateKeys();
    }
    this.nextProcess();
  }
}

class PublicKeySend extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "publicKeySend"));
    var pdu = ProxyPdu.prototype.deriveProvisioningPublicKey({
      publicKeyX: this.generateKeys.publicKeyX.toString(16), publicKeyY: this.generateKeys.publicKeyY.toString(16)
    });
    this.setPdu(pdu);
    this.writeProvisionValue();
  }
}

class Ecdh extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "ecdh"));
    var PublicKey = this.publicKeyReceive.pdu;
    var devicePublicKeyX = PublicKey.publicKeyX;
    var devicePublicKeyY = PublicKey.publicKeyY;
    [this.secretX, this.secretY] = ECDH.prototype.secret_key(this.generateKeys.privateKey, devicePublicKeyX, devicePublicKeyY);
    this.nextProcess();
  }
}

class ConfirmationSend extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "confirmationSend"));
    var ECDHSecret = this.ecdh.secretX.toString(16);
    var publicKeyProvisioner = this.generateKeys.publicKeyX.toString(16) + this.generateKeys.publicKeyY.toString(16);
    var PublicKey = this.publicKeyReceive.pdu;
    var devicePublicKeyX = PublicKey.publicKeyX;
    var devicePublicKeyY = PublicKey.publicKeyY;
    var publicKeyDevice = devicePublicKeyX + devicePublicKeyY;
    this.random = utils.bytesToHex(utils.toAsciiCodes(utils.randomString(16)));
    var pdu = ProxyPdu.prototype.deriveProvisioningConfirmation(ECDHSecret, this.invite.pdu.toPdu(), this.capabilities.pdu.toPdu(), this.start.pdu.toPdu(), publicKeyProvisioner, publicKeyDevice, this.random);
    this.setPdu(pdu);
    this.writeProvisionValue();
  }
}

class RandomSend extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "randomSend"));
    var pdu = ProxyPdu.prototype.deriveProvisioningRandom(this.confirmationSend.random);
    this.setPdu(pdu);
    this.writeProvisionValue();
  }
}

class ProvisioningData extends ProvisioningProcess {
  process() {
    this.blemash.emit("provisioning", getResponseMessage(0, "provisioningData"));
    var pdu = ProxyPdu.prototype.deriveProvisioningData({ netKey: this.netKey, keyIndex: this.keyIndex, iVIndex: this.iVIndex, unicastAddress: this.unicastAddress, confirmationSalt: this.confirmationSend.pdu.confirmationSalt, randomProvisioner: this.randomSend.pdu.toPdu(), randomDevice: this.randomReceive.pdu.toPdu(), ECDHSecret: this.ecdh.secretX.toString(16) });
    this.devKey = pdu.devKey;
    this.setPdu(pdu);
    this.writeProvisionValue();
  }
}



class Provisioning {

  constructor(blemash) {
    this.blemash = blemash;
    this.invite = new Invite(blemash,"invite");
    this.capabilities = new Capabilities(blemash,"capabilities");
    this.start = new Start(blemash,"start");
    this.generateKeys = new GenerateKeys(blemash,"generateKeys");

    this.publicKeySend = new PublicKeySend(blemash,"publicKeySend");
    this.publicKeySend.generateKeys = this.generateKeys;

    this.publicKeyReceive = new PublicKeyReceive(blemash,"publicKeyReceive");

    this.ecdh = new Ecdh(blemash,"ecdh");
    this.ecdh.publicKeyReceive = this.publicKeyReceive;
    this.ecdh.generateKeys = this.generateKeys;

    this.confirmationSend = new ConfirmationSend(blemash,"confirmationSend");
    this.confirmationSend.publicKeyReceive = this.publicKeyReceive;
    this.confirmationSend.generateKeys = this.generateKeys;
    this.confirmationSend.ecdh = this.ecdh;
    this.confirmationSend.invite = this.invite;
    this.confirmationSend.capabilities = this.capabilities;
    this.confirmationSend.start = this.start;

    this.confirmationReceive = new ConfirmationReceive(blemash,"confirmationReceive");

    this.randomSend = new RandomSend(blemash,"randomSend");
    this.randomSend.confirmationSend = this.confirmationSend;

    this.randomReceive = new RandomReceive(blemash,"randomReceive");

    this.provisioningData = new ProvisioningData(blemash,"provisioningData");
    this.provisioningData.netKey = blemash.netkey;
    this.provisioningData.keyIndex = "0" + blemash.netkeyIndex;
    this.provisioningData.iVIndex = blemash.iv_index;
    this.provisioningData.confirmationSend = this.confirmationSend;
    this.provisioningData.randomSend = this.randomSend;
    this.provisioningData.randomReceive = this.randomReceive;
    this.provisioningData.ecdh = this.ecdh;

    this.complete = new Complete(blemash,"complete");
  }

  receiveProvisioning(pdu){
    Provisioning.prototype.currentProcess.processAfter(pdu);
  }

  addDevice(address) {
    this.provisioningData.unicastAddress = address;
    this.invite
      .then(this.capabilities)
      .then(this.start)
      .then(this.generateKeys)
      .then(this.publicKeySend)
      .then(this.publicKeyReceive)
      .then(this.ecdh)
      .then(this.confirmationSend)
      .then(this.confirmationReceive)
      .then(this.randomSend)
      .then(this.randomReceive)
      .then(this.provisioningData)
      .then(this.complete)
      .whenComplete(()=>{
        console.log("add device complete");
        this.receiveTimer.close();
        this.blemash.emit("provisioning", getResponseMessage(0, {
          uuid: this.blemash.deviceId,
          deviceKey: this.provisioningData.devKey
        }));
      })
      .catch((errorCode) => {
        this.receiveTimer.close();
        this.blemash.emit("provisioning", getResponseMessage(errorCode));
      })
    this.invite.process();
    this.receiveTimer = new Timer(Date.now(), 10000);
    this.receiveTimer.schdule(() => {
      this.receiveTimer.close();
      this.blemash.emit("provisioning", getResponseMessage("ff"));
    })
  }

}

module.exports = Provisioning